// Copyright 2011 Google Inc. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//     http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

/**
 * @fileoverview Provides functionality for parsing a Chrome Extensions'
 * manifest.json file; returning an object with relevant information.
 *
 * Currently, this model supports the following manifest data members:
 *     name
 *     version
 *
 *     description
 *     icons
 *
 *     background_page (Partially; see below)
 *     content_scripts
 *     browser_action
 *     options_page
 *     permissions
 *
 * Currently not supporting:
 *     default_locale,
 *
 *     themes
 *     apps
 *
 *     chrome_url_overrides
 *     homepage_url
 *     incognito
 *     key
 *     minimum_chrome_version
 *     omnibox
 *     plugins
 *     update_url
 *
 * Additional properties added to the Info Object:
 *     rootUrl
 *     backgroundScript
 *     contentScript
 *
 * Note: background_page is only partially supported because I create a
 * background sandbox but I could not figure out how to create an "invisible"
 * window for the background.  Thus its html is ignored.
 *
 * @author jasonstredwick@google.com (Jason Stredwick)
 */


goog.provide('mimic.manifest');

goog.provide('bite.common.chrome.extension.urlMatching');
goog.require('console');
goog.require('mimic.xhr');


/**
 * Retrieves the user's extension manifest (manifest.json).  It then processes
 * the necessary data elements and extracts the useful information.
 * @param {string} alias The alias used to locate the extension's files within
 *     the extension folder structure.
 * @return {Object} Returns either an object containing the useful data or
 *     null upon error.
 */
mimic.manifest.processManifest = function(alias) {
  var extensionInfo = {rootUrl: 'chrome://' + alias + '/content/'};

  var url = extensionInfo.rootUrl + 'manifest.json';
  var response = mimic.xhr.requestFile(url, 'text/plain');
  if (response.error) {
    console.error('mimic.manifest.processManifest - ' + response.text);
    return null;
  }

  try {
    var manifest = JSON.parse(response.text);
  } catch (error) {
    console.error('mimic.manifest.processManifest - Failed to parse ' +
                  'manifest.json.');
    return null;
  }

  // Check for required properties and validate the version number.
  if (!('name' in manifest && 'version' in manifest && manifest['name'] &&
      mimic.manifest.validateVersion_(manifest['version']))) {
    console.error('mimic.manifest.processManifest - Missing or invalid ' +
                  'required property(s): "name", "version".');
    return null;
  }

  // Record information, but for some only add it if valid and present.
  extensionInfo.name = manifest['name'];
  extensionInfo.version = manifest['version'];
  extensionInfo.icons = manifest['icons'] || {};
  extensionInfo.permissions = manifest['permissions'] || [];
  extensionInfo.browserAction = manifest['browser_action'] || null;
  if ('description' in manifest) {
    extensionInfo.description = manifest['description'];
  }
  if ('background_page' in manifest) {
    var filename = manifest['background_page'];
    if (!filename) {
      console.error('mimic.manifest.processManifest - Missing ' +
                    'filename for background page.');
      return null;
    } else if (filename[0] == '/') {
      filename = filename.substr(1);
    }

    extensionInfo.backgroundPage = extensionInfo.rootUrl + filename;
    extensionInfo.backgroundScript =
        mimic.manifest.processBackgroundPage_(extensionInfo.backgroundPage,
                                              extensionInfo.rootUrl);
    if (extensionInfo.backgroundScript == null) {
      // Function will report its own errors.
      return null;
    }
  }
  if ('options_page' in manifest) {
    extensionInfo.optionsPage = manifest['options_page'];
  }
  if ('content_scripts' in manifest) {
    extensionInfo.contentScripts =
        mimic.manifest.processContentScripts_(manifest['content_scripts'],
                                              extensionInfo.rootUrl);
    if (extensionInfo.contentScripts == null) {
      // Function will report its own errors.
      return null;
    }
  }

  return extensionInfo;
};


/**
 * Retrieves the html file (if given) for the background page, strips out all
 * the script tags from the head, and creates a string concatenation of all
 * the script code.
 * @param {string} url The url for the background page's html.
 * @param {string} rootUrl The rootUrl for the user's extension.
 * @return {?string} Returns null upon error, ' ' if there is no script code
 *     found, or a string formed by the concatenation of all script code.
 * @private
 */
mimic.manifest.processBackgroundPage_ = function(url, rootUrl) {
  var htmlPath = url.replace(/[/][^/]+[.]html$/, '/');
  var response = mimic.xhr.requestFile(url, 'text/plain');
  if (response.error) {
    console.error('mimic.manifest.processBackgroundPage_ - ' + response.text);
    return null;
  }

  var html = response.text;
  var head = html.match(/<head>([\s\S]*)<[/]head>/i);
  if (!head || head.length < 2) {
    // A background page with no scripts was specified, so create an empty
    // filler script.
    return ' ';
  }
  head = head[1];

  var scripts = head.match(/<script[^>]*([/]>|>[^<]*<[/]script>)/ig);
  if (!scripts || !scripts.length) {
    // A background page with no scripts was specified, so create an empty
    // filler script.
    return ' ';
  }

  var backgroundScript = '';

  // Load all the background scripts into a single string.  It assumes that
  // all scripts are local files or text within a script tag.
  for (var i = 0; i < scripts.length; ++i) {
    var scriptInfo =
        scripts[i].match(/([sS][rR][cC]\s*=\s*"([^"]*)"|>([^<]*)<)/);

    /**
     * scriptInfo holds an array of matched patterns.  Index 2 will hold
     * the relative url for the script or undefined.  Index 3 will hold the
     * script text between the open/close script tag or undefined.
     * Note: If url is given it takes priority over script text.
     */
    var src = '';
    if (scriptInfo[2]) {
      url = (scriptInfo[2][0] == '/') ?
          rootUrl + scriptInfo[2].substr(1) : htmlPath + scriptInfo[2];

      response = mimic.xhr.requestFile(url, 'text/plain');
      if (response.error) {
        console.error('mimic.manifest.processBackgroundPage_ - ' +
                      response.text);
        return null;
      }

      backgroundScript += response.text;

    } else if (scriptInfo[3]) {
      backgroundScript += scriptInfo[3];
    }
  }

  return backgroundScript || ' ';
};


/**
 * Parses the content script data into a more prepared form.
 * @param {Array.<Object>} data The content script array data.
 * @param {string} rootUrl The rootUrl for the user's extension.
 * @return {Array.<Object>} Returns null upon error or an Object containing the
 *     processed content scripts.
 * @private
 */
mimic.manifest.processContentScripts_ = function(data, rootUrl) {
  if (!data || !('length' in data)) {
    console.error('mimic.manifest.processContentScripts_ - Invalid ' +
                  'content scripts data.');
    return null;
  }

  // Create alias to the UrlMatching namespace.
  var urlMatching = bite.common.chrome.extension.urlMatching;

  var scripts = [];
  for (var i = 0; i < data.length; ++i) {
    var contentScript = data[i];
    if (typeof(contentScript) != 'object') {
      console.error('mimic.manifest.processContentScripts_ - Invalid ' +
                    'content script object at index ' + i);
      return null;
    }

    if (!('matches' in contentScript)) {
      console.error('mimic.manifest.processContentScripts_ - Script[' + i +
                    '] is missing required field "matches".');
      return null;
    }

    var matchRegExp = [];
    var matches = contentScript['matches'];
    for (var j = 0; j < matches.length; ++j) {
      try {
        var convertedExp = urlMatching.convertToRegExp(matches[j]);
        if (!convertedExp) {
          throw 'mimic.manifest.processContentScripts_ - Script[' + i +
                '] has a bad match pattern - ' + matches[j];
        }
        matchRegExp.push(convertedExp);
      } catch (error) {
        console.error(error);
        return null;
      }
    }

    var entry, url, response;

    var css = '';
    var cssFiles = contentScript['css'];
    for (j = 0; cssFiles && j < cssFiles.length; ++j) {
      entry = cssFiles[j];
      if (typeof(entry) != 'string') {
        console.error('mimic.manifest.processContentScripts_ - Script[' + i +
                      '] has an invalid css entry - ' + entry);
        return null;
      }

      var cssEntry = (entry[0] == '/') ? entry.substr(1) : entry;
      url = rootUrl + cssEntry;

      response = mimic.xhr.requestFile(url, 'text/plain');
      if (response.error) {
        console.error('mimic.manifest.processContentScripts_ - ' +
                      response.text);
        return null;
      }

      css += response.text;
    }

    var js = '';
    var jsFiles = contentScript['js'];
    for (j = 0; jsFiles && j < jsFiles.length; ++j) {
      entry = jsFiles[j];
      if (typeof(entry) != 'string') {
        console.error('mimic.manifest.processContentScripts_ - Script[' + i +
                      '] has an invalid js entry - ' + entry);
        return null;
      }

      var jsEntry = (entry[0] == '/') ? entry.substr(1) : entry;
      url = rootUrl + jsEntry;

      response = mimic.xhr.requestFile(url, 'text/plain');
      if (response.error) {
        console.error('mimic.manifest.processContentScripts_ - ' +
                      response.text);
        return null;
      }

      js += response.text;
    }

    var script = {matches: matchRegExp, css: css, js: js};
    scripts.push(script);
  }

  return scripts;
};


/**
 * Validates a given string to ensure it meets the requirements for a version
 * string.
 * @param {string} version The version string to validate.
 * @return {boolean} Whether or not the version string is valid.
 * @private
 */
mimic.manifest.validateVersion_ = function(version) {
  // Create the regular expression to validate the version string.
  var number = '(0|[1-9]\\d*)'; // 0 or 10302 (can't start with 0, except 0)
  var oneDeep = number;                              // d
  var twoDeep = number + '([.]' + oneDeep + ')?';    // c or c.d
  var threeDeep = number + '([.]' + twoDeep + ')?';  // b or b.c or b.c.d
  var fourDeep = number + '([.]' + threeDeep + ')?'; // a or a.b or a.b.c or
                                                     // a.b.c.d
  var pattern = '';
  try {
    pattern = new RegExp('^' + fourDeep + '$');
  } catch (error) {
    console.error('mimic.manifest.validateVersion - ' + error);
    return false;
  }

  var results = pattern.exec(version);
  if (!results) {
    console.error('mimic.manifest.validateVersion - Failed pattern test.');
    return false;
  }

  // Process each of the version numbers found in the string.
  for (var i = 0; i < 4; ++i) {
    var value = results[i * 2 + 1]; // 1, 3, 5, 7

    // If the value is undefined then that means there is no more version
    // information in the rest of the string, so stop checking.
    if (!value) {
      break;
    }

    // Version numbers can not exceed 65535.
    if (value > 65535) {
      console.error('mimic.manifest.validateVersion - Version component[' +
                    i + '] exceeds numeric restriction of ' + '[0..65535] ' +
                    'with value of ' + value + '.');
      return false;
    }
  }

  return true;
};

